#!/usr/bin/env bash

usage () {
    echo "Synopsis: re2c test script."
    echo ""
    echo "Available options:"
    echo "  --help                 show this help"
    echo "  --valgrind             run ./re2c under valgrind"
    echo "  --skeleton             run skeleton validation for sources that support it"
    echo "  --keep-temp-files      don't delete temporary files after test run"
    echo "  --wine                 run ./re2c.exe under wine"
    echo "  -j<N>                  override CPU autodetection and use specified value"
    echo ""
    echo "Usage examples:"
    echo "  - run tests in parallel"
    echo "    \$ ./run_tests.sh"
    exit 1
}

# somewhat portable way to detect CPU count
detect_cpu_count () {
    if [ "$CPUS" = "" ]; then
        # Windows standard environment variable
        CPUS="$NUMBER_OF_PROCESSORS"
    fi
    if [ "$CPUS" = "" ]; then
        # Linux
        CPUS=`getconf _NPROCESSORS_ONLN 2>/dev/null`
    fi
    if [ "$CPUS" = "" ]; then
        # FreeBSD
        CPUS=`getconf NPROCESSORS_ONLN 2>/dev/null`
    fi
    if [ "$CPUS" = "" ]; then
        # nothing helped
        CPUS="1"
    fi
}

lc_run() {
    LANG=C LC_ALL=C "$@"
}

re2c="./re2c"
diff_prog="diff"
valgrind=""
skeleton=0
wine=""
threads=`detect_cpu_count; echo $CPUS`
tests=()
for arg in $*
do
    case $arg in
        "--help" ) usage ;;
        "--valgrind" ) valgrind=`which valgrind` ;;
        "--skeleton" ) skeleton=1 ;;
        "--wine" )
            wine=`which wine`
            re2c="${re2c}.exe"
            diff_prog="${diff_prog} -b" # ignore whitespace at the end of line
            ;;
        "-j"* )
            number=${arg#-j}
            number_pattern='^[0-9]+$'
            if [[ $number =~ $number_pattern ]]
            then
                threads=$number
            fi
            ;;
        * ) tests[${#tests[@]}]="$arg" ;; # array is continuous (old bash lacks +=)
    esac
done
echo "Running in ${threads} thread(s)"

if test ! -x "${re2c}"; then
    echo "Cannot find re2c executable (${re2c})."
    exit 1
fi

test_blddir="test_"`date +%y%m%d%H%M%S`
rm -rf $test_blddir && mkdir $test_blddir

# preserve directory structure unless given explicit args
if [ ${#tests[@]} -eq 0 ]; then
    cp -R "@top_srcdir@/test/"* "@top_srcdir@/examples/"*  $test_blddir
else
    for f in ${tests[@]}; do
        cp ${f%.re}.* $test_blddir
    done
fi

chmod -R u+w $test_blddir

find $test_blddir -type f \
    ! -name '*.re' -a \
    ! -name '*.c' -a \
    ! -name '*.h' -a \
    ! -name '*.go' -a \
    ! -name '*.inc' \
    -exec rm {} \;

# if not a debug build, remove all debug subdirs
$re2c --version | grep -q "debug" \
    || find "$test_blddir" -type d -name debug | xargs rm -rf

tests=(`find $test_blddir -name '*.re' | sort`)

# set include paths, relative to build directory
cd $test_blddir && incpaths=$(find * -type d -exec echo "-I {}" \;) && cd ..
# add path to include directory (if relative, add "../" to step out of test subdirectory)
case "@top_srcdir@" in
    /*) incpaths="$incpaths -I @top_srcdir@/include" ;;
    *)  incpaths="$incpaths -I ../@top_srcdir@/include" ;;
esac

tests_per_thread=$((${#tests[@]} / threads + 1))
packs=()
for ((i = 0; i < threads; i++))
do
    j=$((i * tests_per_thread))
    packs[$i]=${tests[@]:j:tests_per_thread}
done

if test -n "${valgrind}"
then
    valgrind_options=(
        "-q"
        "--track-origins=yes"
        "--num-callers=50"
        "--leak-check=full"
        "--show-reachable=yes"
        "--malloc-fill=0xa1"
        "--free-fill=0xa1"
        )
    valgrind="${valgrind} ${valgrind_options[@]}"
    echo $valgrind
fi

run_pack() {
    local log="$1"
    shift 1

    # counters
    local ran_tests=0
    local hard_errors=0
    local soft_errors=0

    for x in $*
    do
        cd $test_blddir

        # remove prefix
        local outx=${x:$((${#test_blddir} + 1))}

        # generate file extension (.c for C/C++, .go for Go)
        local ext=`head -n 1 "$outx" | lc_run grep -q 're2go' && echo go || echo c`
        local outy="${outx%.re}.$ext"

        # Keep to the POSIX standard: no syntactic sugar like +,?, etc. (try it with --posix)
        local switches=`head -n 1 "$outx" | lc_run sed \
            -e 's,re2go,re2c --lang go,' \
            -e 's,.*re2c \(.*\)$,\1,' \
            -e 's,$INPUT,'"$outx"',' \
            -e 's,$OUTPUT,'"$outy"',' \
            -e 's,\(--type-header \)\([^ ]*\),\1'"$(dirname $outx)"'/\2,' \
        `
        # enable warnings globally
        local switches="$switches -W --no-version --no-generation-date"

        # normal tests
        if [ $skeleton -ne 1 ]; then
            # copy expected result
            local orig="${outx%.re}.orig.$ext"
            mv "$outy" "$orig"

            # if options contain a header, create directory structure for it
            local header=`echo "$switches" \
                | lc_run grep -e '--type-header' \
                | lc_run sed -e 's/.*--type-header \([^ ]*\).*/\1/'`
            [ -n "$header" ] && mkdir -p "$(dirname $header)"

            # run re2c
            $valgrind $wine ../$re2c $incpaths $switches 2>"$outy.stderr" 1>&2

            # on windows output contains CR LF, cut CR to match test results
            for f in "$header" "$outy" "$outy.stderr"; do
                [ -f "$f" ] && cat "$f" | lc_run tr -d '\r' > "$f".mod && mv "$f".mod "$f"
            done

            # paste all files created by re2c into output file
            # (this includes stderr and optionally header and skeleton files)
            echo "$header" "$outy".* | lc_run sort | xargs cat >> "$outy"

            # compare results
            local status=""
            [ -z $status ] && status=`[ -f "$orig" ] || echo "MISSING"`
            [ -z $status ] && status=`$diff_prog "$orig" "$outy" > "$outy.diff" || echo "FAIL"`
            [ -z $status ] && status="OK"
            printf "%-10s $outx\n" "$status"
            ran_tests=$(($ran_tests + 1))

            # cleanup
            if [ $status = "OK" ]; then
                echo "$header" "$outy" "$outy".* "$orig" "$outx" | xargs rm
            else
                hard_errors=$(($hard_errors + 1))
            fi

        # skeleton tests (only for C files, other languages are not supported)
        elif [ ${ext} = "c" ]; then
            rm -f "$outy"

            local switches="$switches --skeleton -Werror-undefined-control-flow"

            ${valgrind} ${wine} ../${re2c} $incpaths $switches 2>"$outy.stderr"
            local status=$(echo $?)
            # use plain 'cc' instead of @-substition, because CMake and Autoconf
            # use different variables (CMAKE_C_COMPILER and CC)
            [ $status -eq 0 ] && { cc -Wall -Wextra -o "$outy.out" "$outy" 2>>"$outy.stderr" || status=2; }
            [ $status -eq 0 ] && { ./"$outy.out" 2>>"$outy.stderr" || status=3; }

            case $status in
                0 ) local msg="OK" ;;
                1 ) local msg="OK (expected re2c error)" ;;
                2 ) local msg="FAIL (compilation error)" ;;
                3 ) local msg="FAIL (runtime error)" ;;
                * ) local msg="FAIL (unknown error)" ;;
            esac
            printf "%-25s $outx\n" "$msg"

            case $status in
                0 | 1 ) rm "$outx" "$outy"* ;;
            esac

            ran_tests=$(($ran_tests + 1))
            case $status in
                0 ) ;;
                1 ) soft_errors=$(($soft_errors + 1)) ;;
                * ) hard_errors=$(($hard_errors + 1)) ;;
            esac

        # skeleton tests for unsupported language; cleanup temporary files
        else
            rm "$outx" "$outy"
        fi
        cd ..
    done

    # log results
    {
        echo "ran tests:   $ran_tests"
        echo "hard errors: $hard_errors"
        echo "soft errors: $soft_errors"
    } > "$log"
}

cleanup() {
    rm -f ${logs[@]}
    kill ${wait_pids[@]}
    wait ${wait_pids[@]}
    printf "\nEh...\n"
    exit 1
}

grep_count() {
    cat "$1" | lc_run grep "$2" | lc_run grep -E '[0-9]+' -o
}

logs=()
wait_pids=()
trap cleanup INT
for ((i = 0; i < ${#packs[@]}; i++)); do
    logs[$i]=`date +%y%m%d%H%M%S`_$i.log
    run_pack ${logs[i]} ${packs[i]} &
    wait_pids[${#wait_pids[@]}]=$! # array is continuous (old bash lacks +=)
done
wait ${wait_pids[@]}

# collect results from all threads
total=${#tests[@]}
total_ran_tests=0
total_hard_errors=0
total_soft_errors=0
for ((i = 0; i < ${#logs[@]}; i++)); do
    log="${logs[i]}"
    total_ran_tests=$(($total_ran_tests + $(grep_count "$log" ran)))
    total_hard_errors=$(($total_hard_errors + $(grep_count "$log" hard)))
    total_soft_errors=$(($total_soft_errors + $(grep_count "$log" soft)))
    rm -f "$log"
done

# remove directories that are empty or contain only .inc, .h and .go files
for d in $(find $test_blddir -depth -type d); do
    [ -z "$(find $d -type f ! \( -name '*.inc' -o -name '*.h' -o -name '*.go' \) )" ] && rm -rf "$d"
done

# report results
echo "-----------------"
echo "All:         $total"
echo "Ran:         $total_ran_tests"
echo "Passed:      $(($total_ran_tests - $total_hard_errors - $total_soft_errors))"
echo "Soft errors: $total_soft_errors"
echo "Hard errors: $total_hard_errors"
echo "-----------------"
if [ $total_hard_errors -ne 0 ]; then
    echo "FAILED"
    exit 1
else
    echo "PASSED"
    exit 0
fi
